package catalog

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"text/tabwriter"
	"time"

	"github.com/alicebob/sqlittle"
	"github.com/distribution/distribution/v3"
	"github.com/distribution/distribution/v3/manifest/manifestlist"
	"github.com/joelanford/ignore"
	"github.com/opencontainers/go-digest"
	"github.com/spf13/cobra"

	apicfgv1 "github.com/openshift/api/config/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/util/errors"
	utilyaml "k8s.io/apimachinery/pkg/util/yaml"
	"k8s.io/cli-runtime/pkg/genericiooptions"
	"k8s.io/klog/v2"
	kcmdutil "k8s.io/kubectl/pkg/cmd/util"
	"k8s.io/kubectl/pkg/util/templates"
	"sigs.k8s.io/yaml"

	operatorv1alpha1 "github.com/openshift/api/operator/v1alpha1"
	imgextract "github.com/openshift/oc/pkg/cli/image/extract"
	"github.com/openshift/oc/pkg/cli/image/imagesource"
	"github.com/openshift/oc/pkg/cli/image/info"
	imagemanifest "github.com/openshift/oc/pkg/cli/image/manifest"
	imgmirror "github.com/openshift/oc/pkg/cli/image/mirror"
)

var (
	sqliteDeprecationNotice = `
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! DEPRECATION NOTICE:
!!   Sqlite-based catalogs are deprecated. Support for them will be removed in a
!!   future release. Please migrate your catalog workflows to the new file-based
!!   catalog format.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

`

	mirrorLong = templates.LongDesc(`
		Mirrors the contents of a catalog into a registry.

		This command will pull down an image containing a catalog, extract it to disk, query it to find
		all of the images used in the manifests, and then mirror them to a target registry.

		By default, the catalog files are extracted to a temporary directory, but can be saved locally via flags.

		An image content source policy is written to a file that can be added to a cluster with access to the target
		registry. This will configure the cluster to pull from the mirrors instead of the locations referenced in
		the operator manifests.

		A mapping.txt file is also created that is compatible with "oc image mirror". This may be used to further
		customize the mirroring configuration, but should not be needed in normal circumstances.

	` + prefixLines(sqliteDeprecationNotice, "\t\t\t"))
	mirrorExample = templates.Examples(`
		# Mirror an operator-registry image and its contents to a registry
		oc adm catalog mirror quay.io/my/image:latest myregistry.com

		# Mirror an operator-registry image and its contents to a particular namespace in a registry
		oc adm catalog mirror quay.io/my/image:latest myregistry.com/my-namespace

		# Mirror to an airgapped registry by first mirroring to files
		oc adm catalog mirror quay.io/my/image:latest file:///local/index
		oc adm catalog mirror file:///local/index/my/image:latest my-airgapped-registry.com

		# Configure a cluster to use a mirrored registry
		oc apply -f manifests/imageDigestMirrorSet.yaml

		# Edit the mirroring mappings and mirror with "oc image mirror" manually
		oc adm catalog mirror --manifests-only quay.io/my/image:latest myregistry.com
		oc image mirror -f manifests/mapping.txt

		# Delete all ImageDigestMirrorSets generated by oc adm catalog mirror
		oc delete imagedigestmirrorset -l operators.openshift.org/catalog=true
	`)
)

const (
	DatabaseLocationLabelKey = "operators.operatorframework.io.index.database.v1"
	ConfigsLocationLabelKey  = "operators.operatorframework.io.index.configs.v1"
	IndexLocationLabelKey    = "operators.operatorframework.io.index.database.v1"
	icspKind                 = "ImageContentSourcePolicy"
	idmsKind                 = "ImageDigestMirrorSet"
	minICSPSize              = 0
	maxICSPSize              = 250000
	minIDMSSize              = 0
	maxIDMSSize              = 250000
)

func init() {
	subCommands = append(subCommands, NewMirrorCatalog)
}

type MirrorCatalogOptions struct {
	*IndexImageMirrorerOptions
	genericiooptions.IOStreams

	DryRun          bool
	ManifestOnly    bool
	IndexPath       string
	TempDir         bool
	ContinueOnError bool

	FromFileDir string
	FileDir     string
	MaxICSPSize int
	MaxIDMSSize int

	IcspScope string
	IdmsScope string

	SecurityOptions imagemanifest.SecurityOptions
	FilterOptions   imagemanifest.FilterOptions
	ParallelOptions imagemanifest.ParallelOptions

	SourceRef imagesource.TypedImageReference
	DestRef   imagesource.TypedImageReference
}

func NewMirrorCatalogOptions(streams genericiooptions.IOStreams) *MirrorCatalogOptions {
	return &MirrorCatalogOptions{
		IOStreams:                 streams,
		IndexImageMirrorerOptions: DefaultImageIndexMirrorerOptions(),
		ParallelOptions:           imagemanifest.ParallelOptions{MaxPerRegistry: 4},
		IcspScope:                 "repository",
		IdmsScope:                 "repository",
	}
}

func NewMirrorCatalog(f kcmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
	o := NewMirrorCatalogOptions(streams)

	cmd := &cobra.Command{
		Use:     "mirror SRC DEST",
		Short:   "Mirror an operator-registry catalog",
		Long:    mirrorLong,
		Example: mirrorExample,
		Run: func(cmd *cobra.Command, args []string) {
			kcmdutil.CheckErr(o.Complete(cmd, args))
			kcmdutil.CheckErr(o.Validate())
			kcmdutil.CheckErr(o.Run())
		},
	}
	flags := cmd.Flags()

	o.SecurityOptions.Bind(flags)
	o.ParallelOptions.Bind(flags)

	// Images referenced by catalogs must have all variants mirrored. FilterByOs will only apply to the initial index
	// image, to indicate which arch should be used to extract the catalog index (the index inside should be the same
	// for all arches, so this flag should never need to be set explicitly for standard workflows).
	// this flag is renamed to make it clear that the underlying images are not filtered
	flags.StringVar(&o.FilterOptions.FilterByOS, "index-filter-by-os", o.FilterOptions.FilterByOS, "A regular expression to control which index image is picked when multiple variants are available. Images will be passed as '<platform>/<architecture>[/<variant>]'. This does not apply to images referenced by the index.")

	// the old flag name is kept for backwards-compatibility.
	// if both old and new are specified, the value of the flag coming later will be used.
	flags.StringVar(&o.FilterOptions.FilterByOS, "filter-by-os", o.FilterOptions.FilterByOS, "Use --index-filter-by-os instead. A regular expression to control which index image is picked when multiple variants are available. Images will be passed as '<platform>/<architecture>[/<variant>]'. This does not apply to images referenced by the index.")
	_ = flags.MarkDeprecated("filter-by-os", "use --index-filter-by-os instead")

	flags.StringVar(&o.ManifestDir, "to-manifests", "", "Local path to store manifests.")
	flags.StringVar(&o.IndexPath, "path", "", "Specify an in-container to local path mapping for the index file(s).")
	flags.BoolVar(&o.DryRun, "dry-run", o.DryRun, "Print the actions that would be taken and exit without writing to the destinations.")
	flags.BoolVar(&o.ManifestOnly, "manifests-only", o.ManifestOnly, "Calculate the manifests required for mirroring, but do not actually mirror image content.")
	flags.StringVar(&o.FileDir, "dir", o.FileDir, "The directory on disk that file:// images will be copied under.")
	flags.StringVar(&o.FromFileDir, "from-dir", o.FromFileDir, "The directory on disk that file:// images will be read from. Overrides --dir")
	flags.IntVar(&o.MaxPathComponents, "max-components", 2, "The maximum number of path components allowed in a destination mapping. Example: `quay.io/org/repo` has two path components.")
	flags.StringVar(&o.IcspScope, "icsp-scope", o.IcspScope, "Scope of registry mirrors in imagecontentsourcepolicy file. Allowed values: repository, registry. Defaults to: repository")
	flags.StringVar(&o.IdmsScope, "idms-scope", o.IdmsScope, "Scope of registry mirrors in imagedigestmirrorset file. Allowed values: repository, registry. Defaults to: repository")
	flags.IntVar(&o.MaxICSPSize, "max-icsp-size", maxICSPSize, "The maximum number of bytes for the generated ICSP yaml(s). Defaults to 250000")
	flags.IntVar(&o.MaxIDMSSize, "max-idms-size", maxIDMSSize, "The maximum number of bytes for the generated IDMS yaml(s). Defaults to 250000")
	flags.BoolVar(&o.ContinueOnError, "continue-on-error", true, "If an error occurs while mirroring, keep going and attempt to mirror as much as possible.")
	flags.MarkDeprecated("icsp-scope", "support for it will be removed in a future release.Use --idms-scope instead.")
	flags.MarkDeprecated("max-icsp-size", "support for it will be removed in a future release. Use --max-idms-size instead.")
	return cmd
}

func (o *MirrorCatalogOptions) Complete(cmd *cobra.Command, args []string) error {
	if len(args) < 2 {
		return fmt.Errorf("must specify source and dest")
	}
	src := args[0]
	dest := args[1]

	// default to linux/amd64 for index image, which we generally expect to exist
	pattern := o.FilterOptions.FilterByOS
	if len(pattern) == 0 {
		o.FilterOptions.FilterByOS = "linux/amd64"
	}
	if err := o.FilterOptions.Validate(); err != nil {
		return err
	}

	srcRef, err := imagesource.ParseReference(src)
	if err != nil {
		return err
	}
	o.SourceRef = srcRef
	destRef, err := imagesource.ParseReference(dest)
	if err != nil {
		return err
	}
	o.DestRef = destRef

	// do not modify image names when storing in file://
	// they will be mirrored again into a real registry from the same set of manifests, so renaming will get lost
	if o.DestRef.Type == imagesource.DestinationFile {
		o.MaxPathComponents = 0
	}

	if o.MaxPathComponents == 1 {
		return fmt.Errorf("maxPathComponents must be 0 (no limit) or greater than 1")
	}

	if o.ManifestDir == "" {
		o.ManifestDir = fmt.Sprintf("manifests-%s-%d", o.SourceRef.Ref.Name, time.Now().Unix())
	}

	allmanifests := imagemanifest.FilterOptions{FilterByOS: ".*"}
	if err := allmanifests.Validate(); err != nil {
		return err
	}

	if err := os.MkdirAll(o.ManifestDir, os.ModePerm); err != nil {
		return err
	}

	// try to get the catalog file location label from src, from pkg/image/info
	var image *info.Image
	retriever := &info.ImageRetriever{
		FileDir:         o.FileDir,
		SecurityOptions: o.SecurityOptions,
		ManifestListCallback: func(from string, list *manifestlist.DeserializedManifestList, all map[digest.Digest]distribution.Manifest) (map[digest.Digest]distribution.Manifest, error) {
			filtered := make(map[digest.Digest]distribution.Manifest)
			for _, manifest := range list.Manifests {
				if !o.FilterOptions.Include(&manifest, len(list.Manifests) > 1) {
					klog.V(5).Infof("Skipping image for %#v from %s", manifest.Platform, from)
					continue
				}
				filtered[manifest.Digest] = all[manifest.Digest]
			}
			if len(filtered) == 1 {
				return filtered, nil
			}

			buf := &bytes.Buffer{}
			w := tabwriter.NewWriter(buf, 0, 0, 1, ' ', 0)
			fmt.Fprintf(w, "  OS\tDIGEST\n")
			for _, manifest := range list.Manifests {
				fmt.Fprintf(w, "  %s\t%s\n", imagemanifest.PlatformSpecString(manifest.Platform), manifest.Digest)
			}
			w.Flush()
			return nil, fmt.Errorf("the image is a manifest list and contains multiple images - use --index-filter-by-os to select from:\n\n%s\n", buf.String())
		},

		ImageMetadataCallback: func(from string, i *info.Image, err error) error {
			if err != nil {
				return err
			}
			image = i
			return nil
		},
	}
	if _, err := retriever.Image(context.TODO(), srcRef); err != nil {
		return err
	}

	indexLocation := "/"
	if dcLocation, ok := image.Config.Config.Labels[ConfigsLocationLabelKey]; ok {
		if !strings.HasSuffix(dcLocation, "/") {
			dcLocation += "/"
		}
		indexLocation = dcLocation
		fmt.Fprintf(o.IOStreams.Out, "src image has index label for declarative configs path: %s\n", indexLocation)
	} else if dbLocation, ok := image.Config.Config.Labels[DatabaseLocationLabelKey]; ok {
		fmt.Fprint(o.IOStreams.ErrOut, sqliteDeprecationNotice)

		indexLocation = dbLocation
		fmt.Fprintf(o.IOStreams.Out, "src image has index label for database path: %s\n", indexLocation)
	}

	if o.IndexPath == "" {
		tmpdir, err := os.MkdirTemp("", "")
		if err != nil {
			return err
		}
		o.IndexPath = indexLocation + ":" + tmpdir
		o.TempDir = true
	} else {
		dir := strings.Split(o.IndexPath, ":")
		if len(dir) < 2 {
			return fmt.Errorf("invalid path")
		}
		if err := os.MkdirAll(filepath.Dir(dir[1]), os.ModePerm); err != nil {
			return err
		}
	}
	fmt.Fprintf(o.IOStreams.Out, "using index path mapping: %s\n", o.IndexPath)

	var mirrorer ImageMirrorerFunc
	mirrorer = func(mapping map[imagesource.TypedImageReference]imagesource.TypedImageReference) error {
		mappings := []imgmirror.Mapping{}
		for from, to := range mapping {
			mappings = append(mappings, imgmirror.Mapping{
				Source:      from,
				Destination: to,
			})
		}
		a := imgmirror.NewMirrorImageOptions(o.IOStreams)
		a.SkipMissing = true
		a.ContinueOnError = o.ContinueOnError
		a.DryRun = o.DryRun
		a.SecurityOptions = o.SecurityOptions
		// because images in the catalog are statically referenced by digest,
		// we do not allow filtering for mirroring. this may change if sparse manifestlists are allowed
		// by registries, or if multi-arch management moves into images that can be rewritten on mirror (i.e. the bundle
		// images themselves, not the images referenced inside of the bundle images).
		a.FilterOptions = allmanifests
		a.ParallelOptions = o.ParallelOptions
		a.KeepManifestList = true
		a.Mappings = mappings
		a.SkipMultipleScopes = true
		if err := a.Validate(); err != nil {
			return fmt.Errorf("error configuring image mirroring: %v", err)
		}
		if err := a.Run(); err != nil {
			return fmt.Errorf("error mirroring image: %v", err)
		}
		return nil
	}

	if o.ManifestOnly {
		mirrorer = func(mapping map[imagesource.TypedImageReference]imagesource.TypedImageReference) error {
			return nil
		}
	}
	o.ImageMirrorer = mirrorer
	if _, ok := image.Config.Config.Labels[ConfigsLocationLabelKey]; ok {
		o.IndexExtractor = o.newDeclcfgExtractor(cmd)
		o.RelatedImagesParser = &declcfgRelatedImagesParser{}
	} else {
		o.IndexExtractor = o.newSqliteExtractor(cmd)
		o.RelatedImagesParser = &sqliteRelatedImagesParser{}
	}

	return nil
}

func (o *MirrorCatalogOptions) newSqliteExtractor(cmd *cobra.Command) IndexExtractor {
	return IndexExtractorFunc(func(from imagesource.TypedImageReference) (string, error) {
		e := imgextract.NewExtractOptions(o.IOStreams)
		e.SecurityOptions = o.SecurityOptions
		e.FilterOptions = o.FilterOptions
		e.ParallelOptions = o.ParallelOptions
		e.FileDir = o.FileDir
		if len(o.FromFileDir) > 0 {
			e.FileDir = o.FromFileDir
		}
		e.Paths = []string{o.IndexPath}
		e.Confirm = true
		if err := e.Complete(cmd, []string{o.SourceRef.String()}); err != nil {
			return "", err
		}
		if err := e.Validate(); err != nil {
			return "", err
		}
		if err := e.Run(); err != nil {
			return "", err
		}
		if len(e.Mappings) < 1 {
			return "", fmt.Errorf("couldn't extract database")
		}

		fmt.Fprintf(o.IOStreams.Out, "wrote database to %s\n", e.Mappings[0].To)
		var dbPath string
		errFound := fmt.Errorf("found valid db file")
		err := filepath.Walk(e.Mappings[0].To, func(path string, info os.FileInfo, err error) error {
			if err != nil {
				return nil
			}
			if info.IsDir() {
				return nil
			}
			if _, err := sqlittle.Open(path); err == nil {
				dbPath = path
				return errFound
			}
			return nil
		})
		if err == errFound {
			fmt.Fprintf(o.IOStreams.Out, "using database at: %s\n", dbPath)
			return dbPath, nil
		}
		if err != nil {
			return "", err
		}
		return "", fmt.Errorf("no database file found in %s", e.Mappings[0].To)
	})
}

func (o *MirrorCatalogOptions) newDeclcfgExtractor(cmd *cobra.Command) IndexExtractor {
	return IndexExtractorFunc(func(from imagesource.TypedImageReference) (string, error) {
		e := imgextract.NewExtractOptions(o.IOStreams)
		e.SecurityOptions = o.SecurityOptions
		e.FilterOptions = o.FilterOptions
		e.ParallelOptions = o.ParallelOptions
		e.FileDir = o.FileDir
		if len(o.FromFileDir) > 0 {
			e.FileDir = o.FromFileDir
		}
		e.Paths = []string{o.IndexPath}
		e.Confirm = true
		if err := e.Complete(cmd, []string{o.SourceRef.String()}); err != nil {
			return "", err
		}
		if err := e.Validate(); err != nil {
			return "", err
		}
		if err := e.Run(); err != nil {
			return "", err
		}
		if len(e.Mappings) < 1 {
			return "", fmt.Errorf("couldn't extract declarative configs")
		}

		fmt.Fprintf(o.IOStreams.Out, "wrote declarative configs to %s\n", e.Mappings[0].To)
		fmt.Fprintf(o.IOStreams.Out, "using declarative configs at: %s\n", e.Mappings[0].To)
		return e.Mappings[0].To, nil
	})
}

type sqliteRelatedImagesParser struct{}

func (_ sqliteRelatedImagesParser) Parse(file string) (map[string]struct{}, error) {
	db, err := sqlittle.Open(file)
	if err != nil {
		return nil, err
	}

	// get all images
	var images = make(map[string]struct{}, 0)
	var errs = make([]error, 0)
	reader := func(r sqlittle.Row) {
		var image string
		if err := r.Scan(&image); err != nil {
			errs = append(errs, err)
			return
		}
		if image != "" {
			images[image] = struct{}{}
		}
	}
	if err := db.Select("related_image", reader, "image"); err != nil {
		errs = append(errs, err)
		return nil, errors.NewAggregate(errs)
	}

	// get all bundlepaths
	if err := db.Select("operatorbundle", reader, "bundlepath"); err != nil {
		errs = append(errs, err)
		return nil, errors.NewAggregate(errs)
	}
	return images, nil
}

type declcfgMeta struct {
	Schema        string                `json:"schema"`
	Image         string                `json:"image"`
	RelatedImages []declcfgRelatedImage `json:"relatedImages,omitempty"`
}

type declcfgRelatedImage struct {
	Name  string `json:"name"`
	Image string `json:"image"`
}

type declcfgRelatedImagesParser struct{}

const (
	indexIgnoreFilename = ".indexignore"
)

func (_ declcfgRelatedImagesParser) Parse(root string) (map[string]struct{}, error) {
	rootFS := os.DirFS(root)

	matcher, err := ignore.NewMatcher(rootFS, indexIgnoreFilename)
	if err != nil {
		return nil, err
	}

	relatedImages := map[string]struct{}{}
	if err := fs.WalkDir(rootFS, ".", func(path string, entry fs.DirEntry, err error) error {
		if err != nil {
			return err
		}
		if entry.IsDir() || entry.Name() == indexIgnoreFilename || matcher.Match(path, false) {
			return nil
		}
		f, err := rootFS.Open(path)
		if err != nil {
			return err
		}
		defer f.Close()
		dec := utilyaml.NewYAMLOrJSONDecoder(f, 4096)
		for {
			var blob declcfgMeta
			if err := dec.Decode(&blob); err != nil {
				if err == io.EOF {
					break
				}
				return err
			}
			relatedImages[blob.Image] = struct{}{}
			for _, ri := range blob.RelatedImages {
				relatedImages[ri.Image] = struct{}{}
			}
		}
		return nil
	}); err != nil {
		return nil, err
	}
	delete(relatedImages, "")
	return relatedImages, nil
}

func (o *MirrorCatalogOptions) Validate() error {
	if o.IndexPath == "" {
		return fmt.Errorf("must specify path for index")
	}
	if o.ManifestDir == "" {
		return fmt.Errorf("must specify path for manifests")
	}
	switch o.IcspScope {
	case "repository", "registry":
	default:
		return fmt.Errorf("invalid icsp-scope %s", o.IcspScope)
	}
	switch o.IdmsScope {
	case "repository", "registry":
	default:
		return fmt.Errorf("invalid idms-scope %s", o.IdmsScope)
	}
	if o.MaxICSPSize <= minICSPSize || o.MaxICSPSize > maxICSPSize {
		return fmt.Errorf("provided max-icsp-size of %d must be greater than %d and less than or equal to %d", o.MaxICSPSize, minICSPSize, maxICSPSize)
	}
	if o.MaxIDMSSize <= minIDMSSize || o.MaxIDMSSize > maxIDMSSize {
		return fmt.Errorf("provided max-idms-size of %d must be greater than %d and less than or equal to %d", o.MaxIDMSSize, minIDMSSize, maxIDMSSize)
	}
	return nil
}

func (o *MirrorCatalogOptions) Run() error {
	// Run postRun to clean up after the run
	defer func() {
		if err := o.postRun(); err != nil {
			fmt.Fprintln(o.IOStreams.ErrOut, err.Error())
		}
	}()

	indexMirrorer, err := NewIndexImageMirror(o.IndexImageMirrorerOptions.ToOption(),
		WithSource(o.SourceRef),
		WithDest(o.DestRef),
	)
	if err != nil {
		return err
	}
	mapping, err := indexMirrorer.Mirror()
	if err != nil {
		err = fmt.Errorf("errors during mirroring. the full contents of the catalog may not have been mirrored: %v", err)
		if !o.ContinueOnError {
			return err
		}
		fmt.Fprintln(o.IOStreams.ErrOut, err.Error())
	}

	return WriteManifests(o.IOStreams.Out, o.SourceRef, o.DestRef, o.ManifestDir, o.IcspScope, o.IdmsScope, o.MaxICSPSize, o.MaxIDMSSize, mapping)
}

func getRegistryMapping(out io.Writer, scope string, objectKind string, mapping map[imagesource.TypedImageReference]imagesource.TypedImageReference) map[string]string {
	registryMapping := map[string]string{}
	for k, v := range mapping {
		if len(v.Ref.ID) == 0 {
			fmt.Fprintf(out, "no digest mapping available for %s, skip writing to %s\n", k, objectKind)
			continue
		}
		if scope == "registry" {
			registryMapping[k.Ref.Registry] = v.Ref.Registry
		} else {
			registryMapping[k.Ref.AsRepository().String()] = v.Ref.AsRepository().String()
		}
	}
	return registryMapping
}

func generateICSPs(out io.Writer, source string, icspScope string, maxICSPSize int, mapping map[imagesource.TypedImageReference]imagesource.TypedImageReference) ([][]byte, error) {
	registryMapping := getRegistryMapping(out, icspScope, icspKind, mapping)
	icsps := [][]byte{}

	for i := 0; len(registryMapping) != 0; i++ {
		icsp, err := generateICSP(out, source+"-"+strconv.Itoa(i), maxICSPSize, registryMapping)
		if err != nil {
			return nil, err
		}
		icsps = append(icsps, icsp)
	}
	return icsps, nil
}

func aggregateICSPs(icsps [][]byte) []byte {
	aggregation := []byte{}
	for _, icsp := range icsps {
		aggregation = append(aggregation, []byte("---\n")...)
		aggregation = append(aggregation, icsp...)
	}
	return aggregation
}

func aggregateIDMSs(idmss [][]byte) []byte {
	aggregation := []byte{}
	for _, idms := range idmss {
		aggregation = append(aggregation, []byte("---\n")...)
		aggregation = append(aggregation, idms...)
	}
	return aggregation
}

func generateIDMSs(out io.Writer, source string, idmsScope string, maxIDMSSize int, mapping map[imagesource.TypedImageReference]imagesource.TypedImageReference) ([][]byte, error) {
	registryMapping := getRegistryMapping(out, idmsScope, idmsKind, mapping)
	idmss := [][]byte{}

	for i := 0; len(registryMapping) != 0; i++ {
		idms, err := generateIDMS(out, source+"-"+strconv.Itoa(i), maxIDMSSize, registryMapping)
		if err != nil {
			return nil, err
		}
		idmss = append(idmss, idms)
	}
	return idmss, nil
}

func (o *MirrorCatalogOptions) postRun() error {
	// If we have NOT set --path, the TempDir is set to true and we will delete
	// Temporary folder.
	if !o.TempDir {
		return nil
	}

	dir := strings.Split(o.IndexPath, ":")
	if len(dir) < 2 {
		return fmt.Errorf("error cleaning up temp dir: could not find dir")
	}

	if err := os.RemoveAll(dir[1]); err != nil {
		return fmt.Errorf("error cleaning up temp dir %s: %w", dir[1], err)
	}

	fmt.Fprintf(o.IOStreams.Out, "deleted dir %s\n", dir[1])

	return nil
}

func WriteManifests(out io.Writer, source, dest imagesource.TypedImageReference, dir, icspScope, idmsScope string, maxICSPSize, maxIDMSSize int, mapping map[imagesource.TypedImageReference]imagesource.TypedImageReference) error {
	f, err := os.Create(filepath.Join(dir, "mapping.txt"))
	if err != nil {
		return err
	}
	defer func() {
		if err := f.Close(); err != nil {
			fmt.Fprintf(out, "error closing file\n")
		}
	}()

	if err := writeToMapping(f, mapping); err != nil {
		return err
	}

	if dest.Type != imagesource.DestinationFile {
		idmss, err := generateIDMSs(out, source.Ref.Name, idmsScope, maxIDMSSize, mapping)
		if err != nil {
			return nil
		}

		if err := os.WriteFile(filepath.Join(dir, "imageDigestMirrorSet.yaml"), aggregateIDMSs(idmss), os.ModePerm); err != nil {
			return fmt.Errorf("error writing ImageDigestMirrorSet")
		}

		icsps, err := generateICSPs(out, source.Ref.Name, icspScope, maxICSPSize, mapping)
		if err != nil {
			return err
		}

		if err := os.WriteFile(filepath.Join(dir, "imageContentSourcePolicy.yaml"), aggregateICSPs(icsps), os.ModePerm); err != nil {
			return fmt.Errorf("error writing ImageContentSourcePolicy")
		}

		catalogSource, err := generateCatalogSource(source, mapping)
		if err != nil {
			return err
		}
		if err := os.WriteFile(filepath.Join(dir, "catalogSource.yaml"), catalogSource, os.ModePerm); err != nil {
			return fmt.Errorf("error writing CatalogSource")
		}
	}

	fmt.Fprintf(out, "wrote mirroring manifests to %s\n", dir)

	if dest.Type == imagesource.DestinationFile {
		localIndexLocation, err := mount(source, dest, 0)
		if err != nil {
			return err
		}
		fmt.Fprintf(out, "\nTo upload local images to a registry, run:\n\n\toc adm catalog mirror %s REGISTRY/REPOSITORY\n", localIndexLocation)
	}

	return nil
}

func writeToMapping(w io.StringWriter, mapping map[imagesource.TypedImageReference]imagesource.TypedImageReference) error {
	for k, v := range mapping {
		to := v
		// render with a tag when mirroring so that the target registry doesn't garbage collect the image
		to.Ref.ID = ""
		if _, err := w.WriteString(fmt.Sprintf("%s=%s\n", k.String(), to.String())); err != nil {
			return err
		}
	}

	return nil
}

func generateCatalogSource(source imagesource.TypedImageReference, mapping map[imagesource.TypedImageReference]imagesource.TypedImageReference) ([]byte, error) {
	dest, ok := mapping[source]
	if !ok {
		return nil, fmt.Errorf("no mapping found for index image")
	}
	unstructuredObj := unstructured.Unstructured{
		Object: map[string]interface{}{
			"apiVersion": "operators.coreos.com/v1alpha1",
			"kind":       "CatalogSource",
			"metadata": map[string]interface{}{
				"name":      source.Ref.Name,
				"namespace": "openshift-marketplace",
			},
			"spec": map[string]interface{}{
				"sourceType": "grpc",
				"image":      dest.String(),
			},
		},
	}
	csExample, err := yaml.Marshal(unstructuredObj.Object)
	if err != nil {
		return nil, fmt.Errorf("unable to marshal CatalogSource yaml: %v", err)
	}

	return csExample, nil
}

func generateICSP(out io.Writer, name string, byteLimit int, registryMapping map[string]string) ([]byte, error) {
	icsp := operatorv1alpha1.ImageContentSourcePolicy{
		TypeMeta: metav1.TypeMeta{
			APIVersion: operatorv1alpha1.GroupVersion.String(),
			Kind:       "ImageContentSourcePolicy"},
		ObjectMeta: metav1.ObjectMeta{
			Name: strings.Join(strings.Split(name, "/"), "-"),
			Labels: map[string]string{
				"operators.openshift.org/catalog": "true",
			},
		},
		Spec: operatorv1alpha1.ImageContentSourcePolicySpec{
			RepositoryDigestMirrors: []operatorv1alpha1.RepositoryDigestMirrors{},
		},
	}

	for key := range registryMapping {
		repositoryDigestMirror := operatorv1alpha1.RepositoryDigestMirrors{
			Source:  key,
			Mirrors: []string{registryMapping[key]},
		}
		icsp.Spec.RepositoryDigestMirrors = append(icsp.Spec.RepositoryDigestMirrors, repositoryDigestMirror)
		y, err := yaml.Marshal(icsp)
		if err != nil {
			return nil, fmt.Errorf("unable to marshal ImageContentSourcePolicy yaml: %v", err)
		}

		if len(y) > byteLimit {
			if lenMirrors := len(icsp.Spec.RepositoryDigestMirrors); lenMirrors > 1 {
				icsp.Spec.RepositoryDigestMirrors = icsp.Spec.RepositoryDigestMirrors[:lenMirrors-1]
				break
			}
			return nil, fmt.Errorf("unable to add mirror %v to ICSP with the max-icsp-size set to %d", repositoryDigestMirror, byteLimit)
		}
		delete(registryMapping, key)
	}

	// Create an unstructured object for removing creationTimestamp
	unstructuredObj := unstructured.Unstructured{}
	var err error
	unstructuredObj.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&icsp)
	if err != nil {
		return nil, fmt.Errorf("error converting to unstructured: %v", err)
	}
	delete(unstructuredObj.Object["metadata"].(map[string]interface{}), "creationTimestamp")

	icspExample, err := yaml.Marshal(unstructuredObj.Object)
	if err != nil {
		return nil, fmt.Errorf("unable to marshal ImageContentSourcePolicy yaml: %v", err)
	}

	return icspExample, nil
}

func generateIDMS(out io.Writer, name string, byteLimit int, registryMapping map[string]string) ([]byte, error) {
	idms := apicfgv1.ImageDigestMirrorSet{
		TypeMeta: metav1.TypeMeta{
			APIVersion: apicfgv1.GroupVersion.String(),
			Kind:       "ImageDigestMirrorSet",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: strings.Join(strings.Split(name, "/"), "-"),
			Labels: map[string]string{
				"operators.openshift.org/catalog": "true",
			},
		},
		Spec: apicfgv1.ImageDigestMirrorSetSpec{
			ImageDigestMirrors: []apicfgv1.ImageDigestMirrors{},
		},
	}

	for key := range registryMapping {
		imageDigestMirror := apicfgv1.ImageDigestMirrors{
			Source:  key,
			Mirrors: []apicfgv1.ImageMirror{apicfgv1.ImageMirror(registryMapping[key])},
		}
		idms.Spec.ImageDigestMirrors = append(idms.Spec.ImageDigestMirrors, imageDigestMirror)
		y, err := yaml.Marshal(idms)
		if err != nil {
			return nil, fmt.Errorf("unable to marshal ImageDigestMirrorSet yaml: %v", err)
		}
		if len(y) > byteLimit {
			if lenMirrors := len(idms.Spec.ImageDigestMirrors); lenMirrors > 1 {
				idms.Spec.ImageDigestMirrors = idms.Spec.ImageDigestMirrors[:lenMirrors-1]
				break
			}
			return nil, fmt.Errorf("unable to add mirror %v to IDMS with the max-idms-size set to %d", imageDigestMirror, byteLimit)
		}
		delete(registryMapping, key)
	}

	// Create an unstructured object for removing creationTimestamp, status
	unstructuredObj := unstructured.Unstructured{}
	var err error
	unstructuredObj.Object, err = runtime.DefaultUnstructuredConverter.ToUnstructured(&idms)
	if err != nil {
		return nil, fmt.Errorf("error converting to unstructured: %v", err)
	}
	delete(unstructuredObj.Object["metadata"].(map[string]interface{}), "creationTimestamp")
	delete(unstructuredObj.Object, "status")
	idmsExample, err := yaml.Marshal(unstructuredObj.Object)
	if err != nil {
		return nil, fmt.Errorf("unable to marshal ImageDigestMirrorSet yaml: %v", err)
	}
	return idmsExample, nil
}

func prefixLines(in string, prefix string) string {
	lines := strings.Split(in, "\n")
	for i := range lines {
		lines[i] = fmt.Sprintf("%s%s", prefix, lines[i])
	}
	return strings.Join(lines, "\n")
}
